package com.xes.hardwritingrecognition.opencv;

import android.content.Context;
import android.graphics.Bitmap;
import android.support.annotation.NonNull;
import android.util.Log;

import com.xes.hardwritingrecognition.ImageProcessor;

import org.opencv.android.Utils;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.Point;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.imgproc.Imgproc;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Vector;

import static org.opencv.imgproc.Imgproc.COLOR_BGR2GRAY;

/**
 * Use openCV to process number image and spilt multiple numbers into separate one.
 *
 * @author wl
 * @since 2018/07/06 11:05
 * @deprecated this processor may not be so accurate anymore.
 */
@Deprecated
public class OpenCVNumberProcessor implements ImageProcessor<Vector<Mat>> {
    private static final String TAG = "OpenCVNumberProcessor";

    static {
        System.loadLibrary("opencv_java");
    }

    private static final int CV_FILLED = -1;
    private Context context;
    private boolean isDebugMode;
    private String processTimeStr;

    public OpenCVNumberProcessor(Context context) {
        this.context = context;
    }

    public void setDebugMode(boolean debugMode) {
        isDebugMode = debugMode;
    }

    @Override
    public Vector<Mat> process(@NonNull Bitmap bitmap) {
        processTimeStr = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
        Mat tmpMat = new Mat(bitmap.getHeight(), bitmap.getWidth(), CvType.CV_8UC3);
        Utils.bitmapToMat(bitmap, tmpMat);

        printMat(tmpMat, "original");
        return cuttingImage(tmpMat);
    }

    /**
     * Convert a Mat object into Bitmap, which can be displayed when debugging. If <code>saveFileTag</code>
     * is provided, bitmap will be saved as: {sdcard}/{app-file-dir}/hwrImage/{processTime}/{saveFileTag}.jpg
     *
     * @param mat src Mat in type CV_8Uxx
     * @param saveFileTag if provided, save bitmap
     */
    private void printMat(Mat mat, String saveFileTag) {
        if (isDebugMode) {
            Bitmap bitmap = Bitmap.createBitmap(mat.width(), mat.height(), Bitmap.Config.ARGB_8888);
            Utils.matToBitmap(mat, bitmap);
            Log.d(TAG, "printMat: bitmap" + bitmap);
            if (saveFileTag != null) {
                saveBitmap(bitmap, saveFileTag);
            }
        }
    }

    private void saveBitmap(Bitmap bitmap, String tag) {
        try {
            File dir = new File(context.getExternalFilesDir("hwrImage"), processTimeStr);
            if (!dir.exists()) {
                dir.mkdirs();
            }
            File filePic = new File(dir, tag + ".jpg");
            if (!filePic.exists()) {
                filePic.createNewFile();
            }
            FileOutputStream fos = new FileOutputStream(filePic);
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
            fos.flush();
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //image需要切割的图片
    //功能：对图片进行切割（分割）
    //实现方法：1.对数字5进行处理（如果有的话），2.找到每个数字轮廓，3.对轮廓进行排序，4.降噪,5.尺度变化
    private Vector<Mat> cuttingImage(Mat mat) {
        // 1. 处理数字5
        Mat srcImage = handleNumFive(mat);
        printMat(srcImage, "handleFive");
        // 2. 获取灰度图
        Mat grayImage;
        if (srcImage.channels() == 1) {
            grayImage = srcImage;
        } else {
            grayImage = new Mat();
            Imgproc.cvtColor(srcImage, grayImage, Imgproc.COLOR_BGR2GRAY);//灰度
        }
        printMat(grayImage, null);
        // 3. 二值化
        Mat binaryImage = new Mat();
        Imgproc.threshold(grayImage, binaryImage, 0, 255, Imgproc.THRESH_OTSU);//二值化
        printMat(binaryImage, null);
        // 4. 腐蚀
        Mat dilateImage = new Mat();
        Mat element = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(1, 1));
        Imgproc.dilate(binaryImage, dilateImage, element);//腐蚀
        printMat(dilateImage, null);
        // 5. 获取轮廓
        Vector<MatOfPoint> contours = new Vector<>();
        Mat hierarchy = new Mat();
        Imgproc.findContours(dilateImage, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);//找到轮廓
        // 6. 对轮廓进行排序
        Collections.sort(contours, new Comparator<MatOfPoint>() {
            @Override
            public int compare(MatOfPoint o1, MatOfPoint o2) {
                return sortContours(o1, o2);
            }
        });
        // 遍历每个数字mat
        Vector<Mat> result = new Vector<>();
        for (int i = 0; i < contours.size(); i++) {
            Rect rect = Imgproc.boundingRect(contours.get(i));
            Mat origImage = srcImage.submat(rect).clone(); // 必须clone，防止降噪操作影响原图。
            printMat(origImage, null);
            // 7. 降噪
            Mat denoImage = imageDenoising(origImage);
            printMat(denoImage, null);
            // 8. 缩放
            Mat scaleImage = imageScale(denoImage);
            printMat(scaleImage, "final_" + i);
            // 9. 转float类型
            Mat floatImage = new Mat(28, 28, CvType.CV_32FC1);
            scaleImage.convertTo(floatImage, CvType.CV_32FC1);
            // 10. 1维化
            Mat destImage = floatImage.reshape(0, 1).clone();
            Core.normalize(destImage, destImage, 0.0, 1.0, Core.NORM_MINMAX);
            result.add(destImage);
        }
        return result;
    }

    //srcImage需要调整大小的图像
    //功能：对图像调整成tensorflow的输入大小：该大小为：较长的边调整到20，较短的边等比例调整，上下左右用黑色像素填充，最后图像成为28*28的大小
    private Mat imageScale(Mat srcImage) {
        Mat grayImage = new Mat();
        if (srcImage.channels() == 1) {
            grayImage = srcImage;
        } else {
            Imgproc.cvtColor(srcImage, grayImage, COLOR_BGR2GRAY);//灰度
        }
        int gray_cols = grayImage.cols();
        int gray_rows = grayImage.rows();
        Mat resizeImage = new Mat();
        Mat borderImage = new Mat();
        if (gray_rows >= gray_cols) {
            double h = 20d;
            double w = Math.max(Math.round(h * gray_cols / gray_rows), 1d);
            Imgproc.resize(grayImage, resizeImage, new Size(w, h));
            int left = Math.round((28f - resizeImage.cols()) / 2);
            Imgproc.copyMakeBorder(resizeImage, borderImage, 4, 4, left, 28 - resizeImage.cols() - left, Imgproc.BORDER_CONSTANT, new Scalar(0, 0, 0));
        } else {
            double w = 20d;
            double h = Math.max(Math.round(w * gray_rows / gray_cols), 1d);
            Imgproc.resize(grayImage, resizeImage, new Size(w, h));
            int top = Math.round((28f - resizeImage.rows()) / 2);
            Imgproc.copyMakeBorder(resizeImage, borderImage, top, 28 - resizeImage.rows() - top, 4, 4, Imgproc.BORDER_CONSTANT, new Scalar(0, 0, 0));
        }
        return borderImage;
    }

    //srcImage需要降噪的图片
    //功能：对图片进行降噪————因为找到数字轮廓的时候，是用矩形进行切割的，容易把其他数字的一部分切进来，该函数就是把其他数字的部分去掉，。
    //实现方法：再次找数字的轮廓，如果找到两个轮廓，说明有噪声，需要进行处理，把小的轮廓去掉
    private Mat imageDenoising(Mat srcImage) {
        Mat grayImage;
        if (srcImage.channels() == 1) {
            grayImage = srcImage;
        } else {
            grayImage = new Mat();
            Imgproc.cvtColor(srcImage, grayImage, COLOR_BGR2GRAY);//灰度
        }
        Mat binaryImage = new Mat();
        Imgproc.threshold(grayImage, binaryImage, 0, 255, Imgproc.THRESH_OTSU);

        Mat element = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size (1, 1));
        Mat dilateImage = new Mat();
        Imgproc.dilate(binaryImage, dilateImage, element);
        Vector<MatOfPoint>contours = new Vector<>();
        Mat hierarchy = new Mat();
        Imgproc.findContours(dilateImage, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
        int n = contours.size();
        if (n == 1) {//只有一个轮廓，直接返回该图
            return srcImage;
        } else {//不只有两个轮廓，去掉其中一个轮廓（去掉是噪声的那部分）
            Rect[] r = new Rect[n];
            double max_rect_area = 0d;
            for (int k = 0; k < n; k++) {
                r[k] = Imgproc.boundingRect(contours.get(k));
                if (r[k].area() > max_rect_area) {
                    max_rect_area = r[k].area();
                }
            }
            Vector<MatOfPoint> contours2 = new Vector<>();
            for (int i = 0; i < n; i++) {

                if (r[i].area() < max_rect_area) {
                    contours2.add(contours.get(i));

                }
            }
            Imgproc.drawContours(srcImage, contours2, -1, new Scalar(0, 0, 0), CV_FILLED);
            Imgproc.medianBlur(srcImage, srcImage, 3);
            return srcImage;
        }
    }

    //srcImage传入的数字图片
    //功能：针对数字5单独写的函数，用来判断是否是5的一部分————因为5的特殊性，5有两笔，最上边的那横容易和下边的部分脱离开（像素点不接触），容易把上边的横分割成一个单独的数字，这个函数就是用来解决这个问题
    //实现方法：识别出5的横时，把5的那一横往左移动，使之与5的另一部分接触上
    private Mat handleNumFive(Mat srcImage) {
        Mat grayImage = new Mat();
        if (srcImage.channels() == 1) {//如果是单通道，直接复制
            grayImage = srcImage;
        } else {//如果不是单通道，转为单通道（单通道图像就是灰度图像）
            Imgproc.cvtColor(srcImage, grayImage, COLOR_BGR2GRAY);//灰度
        }
        Mat binaryImage = new Mat();
        Imgproc.threshold(grayImage, binaryImage, 0, 255, Imgproc.THRESH_OTSU);//二值化
        Mat dilateImage = new Mat();
        Mat element = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size (1, 1));
        Imgproc.dilate(binaryImage, dilateImage, element);//膨胀
        Vector<MatOfPoint>contours = new Vector<>();

        Mat hierarchy = new Mat();
        Imgproc.findContours(dilateImage, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);//找到数字的轮廓
        //对轮廓进行排序
        Collections.sort(contours, new Comparator<MatOfPoint>() {
            @Override
            public int compare(MatOfPoint o1, MatOfPoint o2) {
                return sortContours(o1, o2);
            }
        });

        for (int j = 0; j < contours.size(); j++) {
            if (j + 1 < contours.size()) {
                Rect rectnext = Imgproc.boundingRect(contours.get(j + 1));
                double translate = rectnext.height * 1.0 / rectnext.width;//求出倾斜角度
                if (translate < 0.8) {//如果倾斜角度小于0.8，则把横往左移动，让它与另一部分接触
                    MatOfPoint vt = contours.get(j + 1);
                    MatOfPoint newVT = new MatOfPoint();
                    Point fiset = contours.get(j).toArray()[0];
                    double offsetX = fiset.x - rectnext.x;
                    double offsetY = fiset.y - rectnext.y;
                    for (int m = 0; m < vt.toArray().length; m++) {
                        Point oldPoint = vt.toArray()[m];
                        Point newPoint = new Point(oldPoint.x + offsetX, oldPoint.y + offsetY);
                        newVT.push_back(new MatOfPoint(newPoint));
                    }
                    Vector<MatOfPoint> contoursOld = new Vector<>();
                    contoursOld.add(vt);
                    Imgproc.drawContours(srcImage, contoursOld, -1, new Scalar(0), CV_FILLED);
                    Imgproc.drawContours(srcImage, contoursOld, -1, new Scalar(0), CV_FILLED);

                    Vector<MatOfPoint>contoursNew = new Vector<>();
                    contoursNew.add(newVT);
                    Imgproc.drawContours(srcImage, contoursNew, -1, new Scalar(255, 255, 255), CV_FILLED);
                }
            }
        }
        return srcImage;
    }

    //contour1是轮廓1
    //contour2是轮廓2
    //功能：对两个轮廓进行排序————对这两个轮廓左右排序
    private int sortContours(MatOfPoint contour1, MatOfPoint contour2) {
        Rect rect1 = Imgproc.boundingRect(contour1);
        Rect rect2 = Imgproc.boundingRect(contour2);
        if (rect1.x < rect2.x)
            return -1;
        else if (rect1.x == rect2.x)
            return 0;
        else
            return 1;
    }
}
